"""
文件名：example1.py

这里公寓楼吸引房客比例的预测问题举例。
（这里假设房东可以以利益最大化把房价设置为合理值）

假设房租价格和下述几个因素紧密相关：
- （1）房租面积 area；
- （2）房屋楼层 floor；
- （3）层高 height；
- （4）房间数 roomCount
- （5）房屋建设时间 age；
- （6）房屋是否精装修 isP；

另外已知的数据为
- （7）所在小区的历史总租出数量 sumCount；
- （8）公寓楼的历史租出数量 dealCount；

预测的目标值y值为
- （1）公寓租出占小区所有租出数量的比例 ratio；

也就是说，当给出上述（1）-（9）的信息，预测公寓的吸引力比例，作为租赁公司进行采购时的重要参考。

"""
import copy
import datetime
import math
import random
from typing import Dict, List

import pandas

# step1: 写出机理公式，假设机理公式为 ratio = p1 * area + p2 * floor + p3 * height + p4 * roomCount - p5 * age + p6 * isP
print("----------------------------------------------step1 make fit function")


def func_ratio(x_dict: Dict[str, float], param_dict: Dict[str, float]) -> float:
    ratio: float = (
            + x_dict.get("area") * param_dict.get("p1")
            + x_dict.get("floor") * param_dict.get("p2")
            + x_dict.get("height") * param_dict.get("p3")
            + x_dict.get("roomCount") * param_dict.get("p4")
            - x_dict.get("age") * param_dict.get("p5")
            + x_dict.get("isP") * param_dict.get("p6")
    )
    return ratio


# step2: 读取x的历史数据并整合为 dataframe。此例子中采用的是模拟数据，模拟x值
print("\n----------------------------------------------step2 simulate x")

random.seed = 12139

n_point: int = 300  # 数据点数
area_range = (50.0, 200.0)  # 面积范围
floor_set = (1, 2, 3, 4, 5, 6)  # 楼层数集合
height_range = (2.0, 3.0)  # 层高范围
room_count_set = (1, 2, 3)  # 房间数集合
age_range = (1, 70)  # 房龄范围
is_p_set = (0, 1)  # 是否是精装修 取值集合

x_dict_list: List[Dict[str, float]] = [
    dict(
        area=round(random.uniform(*area_range) * 1.e1) * 1.e-1,
        floor=random.choice(floor_set),
        height=round(random.uniform(*height_range) * 1.e2) * 1.e-2,
        roomCount=random.choice(room_count_set),
        age=round(random.uniform(*age_range)),
        isP=random.choice(is_p_set),
    ) for _ in range(n_point)
]
df_x = pandas.DataFrame(x_dict_list)
print(f"-- df_x, shape: {df_x.shape}, head: \n{df_x.head().to_string()}")

# step 3 读取y值的历史数据，此例子中这里模拟了y值

print("\n----------------------------------------------step3 simulate y")

## 设定仿真参数
all_apartment_count_range = (10000, 50000)  # 小区公寓数量随机范围（该值的大小和最终的误差大小紧密相关）
param_dict_simulation = dict(p1=15., p2=1.1, p3=1.5, p4=1.2, p5=2.5, p6=3.0, )
print(f"-- param_dict_simulation before normalize: {param_dict_simulation}")

## 参数归一化
x_dict_best = dict(
    area=area_range[-1],
    floor=floor_set[-1],
    height=height_range[-1],
    roomCount=room_count_set[-1],
    age=age_range[0],
    isP=is_p_set[-1],
)
abs_ratio_abs_upper = func_ratio(
    x_dict=x_dict_best,
    param_dict=param_dict_simulation
)
print(f"-- abs_ratio_abs_upper: {abs_ratio_abs_upper}")
param_dict_simulation = {k: v / abs_ratio_abs_upper for k, v in param_dict_simulation.items()}
print(f"-- param_dict_simulation after normalize: {param_dict_simulation}")
param_list_simulation = [{"name": k, "simulation_value": v} for k, v in param_dict_simulation.items()]
df_param_simulation = pandas.DataFrame(param_list_simulation)
print(f"-- df_param_simulation, shape: {df_param_simulation.shape}, content: \n{df_param_simulation.to_string()}")

## 模拟y值和误差
y_dict_list: List[Dict[str, float]] = []
for i_point, x_dict in enumerate(x_dict_list):
    all_apartment_count = random.randint(*all_apartment_count_range)  # 公寓总数模拟
    ratio_simulation = func_ratio(x_dict=x_dict, param_dict=param_dict_simulation)  # 计算该租赁公司租出占比期望
    dist_mu = max(1.0, ratio_simulation * all_apartment_count)  # 租出数期望
    dist_sigma = math.sqrt(float(dist_mu))  # 租出数一倍误差
    apartment_count = max(1, round(random.gauss(dist_mu, dist_sigma)))  # 租出数模拟
    ratio = round(apartment_count / all_apartment_count * 1.e3) * 1.e-3  # 租出数比例
    ratio_abs_err = round(dist_sigma / all_apartment_count * 1.e3) * 1.e-3  # 比例绝对误差
    y_dict_list.append(copy.copy(dict(ratio=ratio, ratio_abs_err=ratio_abs_err)))  # y值字典
df_y = pandas.DataFrame(y_dict_list)  # y值 dataframe
print(f"-- df_y, shape: {df_y.shape}, head: \n{df_y.head().to_string()}")

# step4 拆分拟合用的数据和检验用的数据
print("\n----------------------------------------------step4 split data_feed and data_test")
n_point_test = 50
n_point_fit = n_point - n_point_test
df_x_fit: pandas.DataFrame = df_x.iloc[:n_point_fit, :]
df_y_fit: pandas.DataFrame = df_y.iloc[:n_point_fit, :]
df_x_test: pandas.DataFrame = df_x.iloc[n_point_fit:, :]
df_y_test: pandas.DataFrame = df_y.iloc[n_point_fit:, :]
print(f"-- n_point_fit={n_point_fit}, n_point_test={n_point_test}"
      f",\n-- df_x_fit shape: {df_x_fit.shape}, df_y_fit shape: {df_y_fit.shape}"
      f",\n-- df_x_test shape: {df_x_test.shape}, df_y_test shape: {df_y_test.shape}")

# step5 拟合
print("\n----------------------------------------------step5 fit")
from profiler.profiler import MultiParamProfiler

## 给定参数范围
param_info_dict_list = [
    {"name": "p1", "init": 1.e-5, "lower": 0., "upper": 1.},
    {"name": "p2", "init": 1.e-5, "lower": 0., "upper": 1.},
    {"name": "p3", "init": 1.e-5, "lower": 0., "upper": 1.},
    {"name": "p4", "init": 1.e-5, "lower": 0., "upper": 1.},
    {"name": "p5", "init": 1.e-5, "lower": 0., "upper": 1.},
    {"name": "p6", "init": 1.e-5, "lower": 0., "upper": 1.},
]
df_param_input = pandas.DataFrame(param_info_dict_list)  # 参数设定值dataframe
print(f"-- df_param_input, shape: {df_param_input.shape}, content: \n{df_param_input.to_string()}")

## 拟合
time_fit_start = datetime.datetime.now()
model: MultiParamProfiler = MultiParamProfiler()
df_param_fitted = model.fit(df_x=df_x_fit, df_y=df_y_fit, df_param=df_param_input,
                            func=func_ratio, y_col="ratio", error_y_col="ratio_abs_err")
print(f"-- df_param_fitted, shape: {df_param_fitted.shape}, content: \n{df_param_fitted}")
time_fit_end = datetime.datetime.now()
cost_time_fit = time_fit_end - time_fit_start
print(f"-- fit cost time seconds: {cost_time_fit.total_seconds()}")

# step6 预测（检验）
print("\n----------------------------------------------step6 predict(test)")
x_dict_test_list: List[Dict[str, float]] = df_x_test.to_dict("records")
y_dict_test_list: List[Dict[str, float]] = df_y_test.to_dict("records")
y_predict_info_dict_list: List[Dict[str, float]] = []
for x_dict_test, y_dict_test in zip(x_dict_test_list, y_dict_test_list):
    y_test = y_dict_test.get("ratio")
    y_predict, _ = model.predict(x_dict=x_dict_test, func_error=None)
    y_predict_info_dict_list.append(copy.copy({
        "ratio_predict": y_predict,
        "ratio_simulation": y_test,
        "diff_absolute": y_predict - y_test,
        "diff_relative": None if y_test == 0 else (y_predict - y_test) / y_test,
    }))
df_y_predict_info = pandas.DataFrame(y_predict_info_dict_list)
print(f"-- df_y_predict_info, shape: {df_y_predict_info.shape}, content: \n{df_y_predict_info.to_string()}")
